<?php
/**
 * 通用TOKEN中心认证类
 * 
 * 
 * 本类提供三个方法供上层使用
 * $this->ParseEncodeToken:解析被客户端软件编码过的Token
 * $this->ParseToken:解析Token内容
 * $this->TokenTransform:转换Token
 * 
 * @author levsion  <levsion@163.com>
 */


require_once(dirname(__FILE__). '/xtea.php');
class TokenCenter {
	
	//token中心服务器配置
	var $TokenServers = array (
		//线上环境
		array ('host' => 'example1.token.bierencom', 'port' => 7003),
		array ('host' => 'example2.token.bierencom', 'port' => 7003),
		array ('host' => 'example3.token.bierencom', 'port' => 7003),
		array ('host' => 'example4.token.bierencom', 'port' => 7003)
		
		//array ('host' => '192.168.200.31', 'port' => 6003)	 //开发环境
	);
	
	var $LocalKey = 'Authentic@bierenshuo.com';	//本地加密KEY
	var $ServerKey = array(		//服务器端加密KEY
			'bierenshuo'		=>	'levsion@bierenshuo.com',
	);
	var $HeadVersion = 0;	//请求包中head区域中附带的版本号
	var $HeadFlag = 4; 		//0000000000000100	请求包中head区域中附带的FLAG信息，现在暂用来标识加密和压缩算法
	var $AuthMsgId = 16;	//0x00010	认证消息ID号
	var $TransformMsgId = 3;//0x00003	TOKEN转换消息ID号
	
	var $AppIds = array(	//各个应用对应的应用ID号
			'bierenshuo'	=>	1
	);
	
	/**
	 * 解析被客户端编码过的TOKEN
	 *
	 * @param string $token 用户token，被客户端编码过的token，非原始TOKEN
	 * @param string $app TOKEN所属的应用,
	 * @return mixed 解析失败返回false,解析成功返回信息数组
	 */
	function ParseEncodeToken($token, $app = 'bierenshuo') {
		$len = strlen($token);
		$token = pack("H" . $len, $token);
		return $this->ParseToken($token, $app);
	}
	
	/**
	 * 解析token中心颁发的原始二进制TOKEN
	 *
	 * @param string $token 用户token
	 * @param string $app TOKEN所属的应用
	 * @return mixed 解析失败返回false,解析成功返回信息数组
	 */
	function ParseToken($token, $app = 'bierenshuo') {
		$xtea = new Xtea_Encrypt();
		$descryptToken = $xtea->Decrypt($token, $this->ServerKey[$app]);
		if($descryptToken == false) return false;
		
		$pos = 0;
		$ret = array();
		
		$ret['sessionkey'] = substr($descryptToken, $pos, 16); // 16B of session key
		$pos += 16;
		
		$ret['uin'] = substr($descryptToken, $pos, 4); // 4B of user oracle id
		$ret['uin'] =  unpack("H*", $ret['uin']);	//PHP5.2.1 , unpack 'N' have a bug on 64bit machines
		$ret['uin'] = hexdec($ret['uin'][1]);
		$pos += 4;

		$userLen = substr($descryptToken, $pos, 1); // 1B of user name len (include \x00)
		$userLen =  unpack("c", $userLen);
		$userLen = $userLen[1];
		$pos += 1;
		$ret['user'] = substr($descryptToken, $pos, $userLen); // the user name
		$ret['user'] = substr($ret['user'], 0, -1);
		$pos += $userLen;
		
		$extraLen = substr($descryptToken, $pos, 1); // 1B of extra user name len (include \x00)
		$extraLen =  unpack("c", $extraLen);
		$extraLen = $extraLen[1];
		$pos += 1;
		$ret['extra'] = substr($descryptToken, $pos, $extraLen); // extra user name
		$ret['extra'] = substr($ret['extra'], 0, -1);
		$pos += $extraLen;
		
		$ret['goodid'] = substr($descryptToken, $pos, 4);	//good id
		$ret['goodid'] =  unpack("H*", $ret['goodid']);
		$ret['goodid'] = hexdec($ret['goodid'][1]);
		$pos += 4;
		
		$ret['servicebitmap'] = substr($descryptToken, $pos, 16); // 1B of signature len
		$pos += 16;

		$ret['time'] = substr($descryptToken, $pos, 4);
		$ret['time'] = unpack("H*", $ret['time']);
		$ret['time'] = hexdec($ret['time'][1]);
		
		return $ret;		
	}
	
	/**
	 * TOKEN转换
	 *
	 * @param string $sourceApp 需要转换的TOKEN来自什么应用
	 * @param string $targetApp 转换到何应用
	 * @param string $token 需要转换的TOKEN
	 * @param float $ctime SOCKET连接超时设置，可为浮点数
	 * @param float $rwtime SOCKET读写超时设置，可为浮点数
	 * @return mixed 转换成功返回新TOKEN，失败返回false
	 */
	function TokenTransform($sourceApp, $targetApp, $token, $ctime = 0.7, $rwtime = 1) {
		$comm = $this->MakeTransformPackage($sourceApp, $targetApp, $token);
		$pear = rand(0, count($this->TokenServers) - 1);	//随机选一台
		$result = $this->SocketCommunication($comm,$this->TokenServers[$pear]['host'],$this->TokenServers[$pear]['port'], $ctime, $rwtime, $error = null);
		if($result == false) {	//失败则会换台服务器重试一次
			$pear = ($pear + 1) % count($this->TokenServers);
			$result = $this->SocketCommunication($comm,$this->TokenServers[$pear]['host'],$this->TokenServers[$pear]['port'],$ctime, $rwtime, $error = null);
		}
		return $this->ParseTransformResult($result);
	}
	
	/**
	 * 生成TOKEN转换请求包
	 *
	 * @param string $sourceApp 从哪一个应用转换
	 * @param string $targetApp 转到哪一个应用
	 * @param string $token 需要转换的TOKEN
	 * @return string 返回转换请求包
	 */
	function MakeTransformPackage($sourceApp, $targetApp, $token) {
		$xtea = new Xtea_Encrypt();
		
		$body = pack('n', $this->AppIds[$sourceApp]) . pack('n',  $this->AppIds[$targetApp]);
		$body .= pack('c', strlen($token));
		$body .= $token;
		$body = $xtea->Encrypt($body, $this->LocalKey); // encrypt the body
		
		// the package head: HTTP(1B) + package len(2B) + head len(2B) + Msg ID(2B) + version(2B) + flag(2B)
		$package = "\x00"; // always \x00
		$totalLen = pack('n', 11 + strlen($body)); // the len of package
		$package .= $totalLen;
		$headLen = pack('n', 11); // the len of package head, 11B
		$package .= $headLen;
		$msgId = pack('n', $this->TransformMsgId); // msg id
		$package .= $msgId;
		$version = pack('n', $this->HeadVersion); // version, always 0
		$package .= $version;
		$flag = pack('n', $this->HeadFlag); //flag
		$package .= $flag;

		$package .= $body;

		return $package;
	}
	
	/**
	 * 解析TOKEN转换响应结果
	 *
	 * @param string $result
	 * @return mixed 解析失败返回false,解析成功返回新TOKEN
	 */
	function ParseTransformResult($result) {
		$xtea = new Xtea_Encrypt();
		
		// parse the head
		$pos = 0;
		$http = substr($result, $pos, 1); // 1B of http
		$http = unpack("X", $http);
		$pos += 1;
		
		$pLen = substr($result, $pos, 2); // 2B of package len
		$pLen =  unpack("n", $pLen);
		$pos += 2;
		if ($pLen[1] != strlen($result)) return false; // the package len was wrong
		
		$hLen = substr($result, $pos, 2); // 2B of head len
		$hLen =  unpack("n", $hLen);
		$pos += 2;
		if($hLen[1] != 11) return false;	//the package head len was wrong
		
		$msgId = substr($result, $pos, 2); // 2B of msg id
		$msgId =  unpack("n", $msgId);
		$pos += 2;
		if($msgId[1] != 4) return false;	//the package msgid was wrong
		
		$version = substr($result, $pos, 2); // 2B of version
		$version =  unpack("n", $version);
		$pos += 2;
		if($version[1] != $this->HeadVersion) return false;		//the package head version was wrong.
		
		$flag = substr($result, $pos, 2); // 2B of flag
		$flag =  unpack("n", $flag);
		$pos += 2;
		if($flag[1] != $this->HeadFlag) return false;	//the package head flag was wrong
		
		// parse the body
		$body = substr($result, $pos);
		$descryptBody = $xtea->Decrypt($body, $this->LocalKey);
		if ($descryptBody !== false) {
			$pos = 0;
			$result = substr($descryptBody, $pos, 2); // 2B of source app
			$pos += 2;
			$result = substr($descryptBody, $pos, 2); // 2B of target app
			$pos += 2;
			$sigLen = substr($descryptBody, $pos, 1); // 1B of target signature len
			$pos += 1;
			$sigLen =  unpack("c", $sigLen);
			$sigLen = $sigLen[1];
			$signature = substr($descryptBody, $pos, $sigLen);
			if($sigLen != strlen($signature)) return false;
			return $signature;
		} else { // parse package error
			return false;
		}
	}
	
	/**
	 * 统一socket处理函数
	 * @param string $comm 请求字符串
	 * @param string $host 连接服务器ip
	 * @param int $port 对应服务器的端口
	 * @param float $ctime 链接超时；此参数为浮点数，如1, 1.0, 1.2均可
	 * @param float $ptime 读写超时；格式和$ctime相同
	 * @param int $protocol socket是几字节的协议；表示前N个字节为返回字符串的长度，当前用的是4字节和6字节
	 * @param string $errorMsg 错误描述
	 * @return 成功返回服务端的返回数据；失败返回false，参数$errorMsg指出当前的错误描述
	 */
	function SocketCommunication($comm, $host, $port, $ctime = 2, $ptime = 2, &$errorMsg) {
		$sock = $this->SocketConnect($host, $port, $ctime, $ptime, $errorMsg);
		if ($sock == false) {
			return false;
		}
		
		$result = $this->SocketWrite($sock, $host, $port, $comm, $errorMsg);
		if ($result == false) {
			socket_close($sock);
			return false;
		}
		
		$response = $this->SocketRead($sock, $host, $port, $errorMsg);
		if ($response == false) {
			socket_close($sock);
			return false;
		}
		socket_close($sock);
		
		return $response;
	}
	
	function SocketConnect($host, $port, $ctime, $ptime, &$errorMsg) {
	    $sock = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
	    if(!$sock) {
	        $errorMsg = "Failed to create socket: $host : $port " . socket_strerror(socket_last_error($sock));
	        return false;
	    }
	
	    socket_set_nonblock($sock);
	    @socket_connect($sock, $host, $port);
	    socket_set_block($sock);
	    $fd_read = array($sock);
	    $fd_write = array($sock);
	    $except = null;
	    $ret = @socket_select($fd_read, $fd_write, $except, floor($ctime), fmod($ctime, 1) * 1000000);
	    if($ret != 1) {
	        if ($ret == 0) {
	        	$errorMsg = "Connection timeout: $host : $port " . socket_strerror(socket_last_error($sock));
	        } elseif ($ret == 2) {
	        	$errorMsg = "Connection refused: $host : $port " . socket_strerror(socket_last_error($sock));
	        } else {
	        	$errorMsg = socket_strerror(socket_last_error($sock)) . ": $host : $port";
	        }
			socket_close($sock);
	        return false;
	    }
	
		// 分别设置读写超时
	    socket_set_option($sock, SOL_SOCKET, SO_SNDTIMEO, array("sec" =>floor($ptime), "usec" => fmod($ptime, 1) * 1000000));
	    socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array("sec" =>floor($ptime), "usec" => fmod($ptime, 1) * 1000000));
		
		return $sock;
	}
	
	function SocketWrite($sock, $host, $port, $comm, &$errorMsg) {
	    // socket写，最多循环20次写
	    $writtenLen = 0; // 已经写的长度
	    $counter = 0; // 循环写的次数
	    $content = $comm;
	    do {
		    $written = @socket_write($sock, $content, strlen($content));
		    if (false === $written) {
		        $errorMsg = "Failed to write data on the socket: $host : $port " . socket_strerror(socket_last_error($sock));
		        return false;
		    }
		    $writtenLen += $written;
		    $counter++;
		    if ($counter > 20) {
	        	$errorMsg = "Exceed 20 time while writing data on socket: $host : $port " . socket_strerror(socket_last_error($sock));
		    	return false;
		    }
		    if ($written < strlen($comm)) {
		    	$content = substr($comm, $writtenLen);
		    }
	    } while ($writtenLen < strlen($comm));
	    
	    return true;
	}
	
	function SocketRead($sock, $host, $port, &$errorMsg) {
	    // 读取返回内容的长度
	    $str = @socket_read($sock, 8190);
	    if ($str == false) { // 接收数据错误或者不完成
			$errorMsg = "Failed to read data on the socket: $host : $port " . socket_strerror(socket_last_error($sock));
			return false;
	    }
	    
		return $str;
	}
}
?>